#include "world_camera.h"
#include "pmodel.h"
#include "sdlmisc.h"
#include "input.h"

using namespace WtfEngine;

#define STOP_EVENT() Kernel::GetInstance().SetEventStatus(Event::STOPPED)


/**** Basic Camera ****/

void Camera::EnterPerspective(const IScreen::tRef& pScreen) const
{
	glPushAttrib(GL_VIEWPORT_BIT);
	glViewport(0,0,pScreen->getSize()[0], pScreen->getSize()[1]);

	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();

	// Camera setup
	gluPerspective(45.0f, // 45 degree field of view
		(tScalar)(pScreen->getSize().x()) / (tScalar)(pScreen->getSize().y()),	// Aspect ratio
		0.1f, 2000.0f);	// Near and far clipping planes
	gluLookAt(mPosition[0],mPosition[1],mPosition[2],
			  mDirection[0],mDirection[1],mDirection[2],
			  mUpDirection[0],mUpDirection[1],mUpDirection[2]);

	glMatrixMode(GL_MODELVIEW);
};

void Camera::LeavePerspective() const
{
	glMatrixMode(GL_PROJECTION);
	glPopMatrix();
	glMatrixMode(GL_MODELVIEW);

	glPopAttrib();
};

tRay Camera::PixelToRay(const IScreen::tRef& pScreen, const iVector2D &vPixel) const {
	
	dVector3D vStart, vEnd;
	double	wx, wy;
	double  vfModelView[16], vfProjection[16];
	GLint	viViewport[16];

	// Get matrices for the camera projection
	EnterPerspective(pScreen);
	glGetDoublev(GL_MODELVIEW_MATRIX, vfModelView);
	glGetDoublev(GL_PROJECTION_MATRIX, vfProjection);
	glGetIntegerv(GL_VIEWPORT, viViewport);
	LeavePerspective();

	wx = vPixel[0];
	wy = pScreen->getSize()[1] - vPixel[1]; // Window coords addressed from top.

	// These functions return world space coordinates based upon the window positions.
	gluUnProject(wx, wy, 0, vfModelView, vfProjection, viViewport, &(vStart.toArray()[0]), &(vStart.toArray()[1]), &(vStart.toArray()[2]));
	gluUnProject(wx, wy, 1.0, vfModelView, vfProjection, viViewport, &(vEnd.toArray()[0]), &(vEnd.toArray()[1]), &(vEnd.toArray()[2]));

	vEnd = (vEnd - vStart);
	return tRay(tVector(vStart[0], vStart[1], vStart[2]), tVector(vEnd[0], vEnd[1], vEnd[2]).normalize());
};


/**** Movable Camera ****/

#define mSpeed 100

void MovableCamera::OnMoveFwd()
{
	tVector v((mDirection - mPosition).normalize() * mSpeed * Kernel::GetInstance().GetGameState()->GetDelta());
	mPosition += v;
	mDirection += v;
};

void MovableCamera::OnMoveBack()
{
	tVector v((mPosition - mDirection).normalize() * mSpeed * Kernel::GetInstance().GetGameState()->GetDelta());
	mPosition += v;
	mDirection += v;
};

void MovableCamera::OnMoveLeft()
{
	tVector v((mPosition - mDirection).cross(mUpDirection).normalize() * mSpeed * Kernel::GetInstance().GetGameState()->GetDelta());
	mPosition += v;
	mDirection += v;
};

void MovableCamera::OnMoveRight()
{
	tVector v((mDirection - mPosition).cross(mUpDirection).normalize() * mSpeed * Kernel::GetInstance().GetGameState()->GetDelta());
	mPosition += v;
	mDirection += v;
};

void MovableCamera::OnLook()
{
	static bool frst;	// HACK to overcome SDL's tendency to send us a massive
						// mouse delta when we startup
	if(frst && SDL_GetKeyState(NULL)[SDLK_LSHIFT]){
		const MouseMoveEvent*	pEv = Kernel::GetInstance().GetCurrentEvent<MouseMoveEvent>();
		
		tScalar	rotLR, rotUD;
		tVector d = (getDirection() - getPosition());

		// Let a change in 800px = rotation of pi radians
		rotLR = (-90.0f / 8000.0f) * (tScalar)pEv->GetDeltaX();
		rotUD = (90.0f / 8000.0f) * (tScalar)pEv->GetDeltaY();
		
		// Create rotation matrix
		Orientation3D ori(Orientation3D::Rotation(mUpDirection, rotLR)
				* Orientation3D::Rotation(mUpDirection.cross(d), rotUD));

		// Rotate direction vectors
		mDirection = mPosition + (ori * d);
	} else frst = true;
};
		
void MovableCamera::BindToKeyboardAndMouse()
{
	GameState *pState = Kernel::GetInstance().GetGameStatePtr();
	
	pState->BindEvent(new KeyDownEvent(InputKey('w', NULL)),
			  new ObjFunctor<MovableCamera>(this, &MovableCamera::OnMoveFwd));
	pState->BindEvent(new KeyDownEvent(InputKey('s', NULL)),
			  new ObjFunctor<MovableCamera>(this, &MovableCamera::OnMoveBack));
	pState->BindEvent(new KeyDownEvent(InputKey('a', NULL)),
			  new ObjFunctor<MovableCamera>(this, &MovableCamera::OnMoveLeft));
	pState->BindEvent(new KeyDownEvent(InputKey('d', NULL)),
			  new ObjFunctor<MovableCamera>(this, &MovableCamera::OnMoveRight));
	pState->BindEvent(new MouseMoveEvent(0,0,0,0),
			  new ObjFunctor<MovableCamera>(this, &MovableCamera::OnLook));
};


/**** Pannable Camera ****/

PannableCamera::PannableCamera(const WtfEngine::PannableCamera::tCreationOptions &opts, const tPosition &pos,
							   const tDirection &direction):
Camera(pos, pos + direction, tVector(0, 1, 0)), mOptions(opts), mbIsPanning(false), mbIsScrolling(false) {
	if(!UpdateFocalPoint())
		throw std::logic_error("Pannable camera: cannot find initial focal point!");

	mfDesiredZoom = (opts.fMaxDistance + opts.fMinDistance) / 2;
	mvDesiredPosition = mPosition;
	IDynamic::Init();
};

void PannableCamera::Scroll(const fVector2D& amt) {
	mvDesiredPosition[0] += amt[0];
	mvDesiredPosition[2] += amt[1];
};

void PannableCamera::Zoom(tScalar amt) {
	mfDesiredZoom = minf(mOptions.fMaxDistance, maxf(mOptions.fMinDistance, mfDesiredZoom - amt));
};

bool PannableCamera::UpdateFocalPoint() {
	// Perform raycast in camera's direction
	tPosition vPos;
	if(PhysicsEngine::GetInstance().RayCast(tRay(mPosition, (mDirection - mPosition).normalize()), vPos).isNull()) {
		return false;
	} else {
		mvFocalPoint = vPos;
		return true;
	};
};


/**
 * Implements smooth scrolling and zooming.
 **/
void PannableCamera::Update() {
	tScalar fCurrentZoom = (mvFocalPoint - mPosition).length();
	tScalar dt = sqrt(Kernel::GetInstance().GetGameStatePtr()->GetDelta());

	if(fCurrentZoom != mfDesiredZoom) {
		// XXX: Here zoom time exponential.
		tVector amt((mDirection - mPosition).normalize() * (fCurrentZoom - mfDesiredZoom) * dt);
		mPosition += amt;
		mvDesiredPosition += amt;
	};

	if(!(mPosition == mvDesiredPosition)) {
		tVector	  amt = mvDesiredPosition - mPosition;
		tPosition vOldPos = mPosition;
		tPosition vOldDir = mDirection;


		amt = amt * dt;
		mPosition += amt;
		mDirection += amt;

		if(!UpdateFocalPoint()) {
			// No focal point - return position to old.
			mPosition = vOldPos;
			mDirection = vOldDir;
			mvDesiredPosition = mPosition;
		};
	};
};

void PannableCamera::OnMouseDown() {
	MouseDownEvent * pEv = Kernel::GetInstance().GetCurrentEvent<MouseDownEvent>();

	switch(pEv->getButton()) {
		case MOUSE_SCROLLUP:
			Zoom(mOptions.fZoomSpeed);
			STOP_EVENT();
			break;
		case MOUSE_SCROLLDOWN:
			Zoom(-mOptions.fZoomSpeed);
			STOP_EVENT();
			break;
		case MOUSE_MIDDLE:
			mbIsPanning = true;
			mbHasPanned = false;
			STOP_EVENT();
			break;
		case MOUSE_RIGHT:
			mbIsScrolling = true;
			mbHasScrolled = false;
			STOP_EVENT();
			break;
	};
};

void PannableCamera::OnMouseMove() {
	MouseMoveEvent * pEv = Kernel::GetInstance().GetCurrentEvent<MouseMoveEvent>();

	if(mbIsScrolling && (mbHasScrolled || (abs(pEv->GetDeltaX() + pEv->GetDeltaY()) > 0))) {
		Scroll(fVector2D(pEv->GetDeltaX() * mOptions.fScrollSpeed, pEv->GetDeltaY() * mOptions.fScrollSpeed));
		
		mbHasScrolled = true;
		STOP_EVENT();
	} else if(mbIsPanning && (mbHasPanned || (abs(pEv->GetDeltaX() + pEv->GetDeltaY()) > 0))) {
		
		mbHasPanned = true;
		STOP_EVENT();
	} else if(mOptions.bEdgeScrolling) {
		if(pEv->x() == 0 || pEv->x() == (Renderer::GetInstance().GetScreen()->getSize()[0] - 1)) {
			// Edge scrolling. TODO: use specified screen ^
			Scroll(fVector2D(pEv->GetDeltaX() * mOptions.fScrollSpeed, 0));
		};
		if(pEv->y() == 0 || pEv->y() == (Renderer::GetInstance().GetScreen()->getSize()[1] - 1)) {
			Scroll(fVector2D(0, pEv->GetDeltaY() * mOptions.fScrollSpeed));
		};
	};
};

void PannableCamera::OnMouseUp() {
	MouseUpEvent * pEv = Kernel::GetInstance().GetCurrentEvent<MouseUpEvent>();

	switch(pEv->getButton()) {
		case MOUSE_MIDDLE:
			mbIsPanning = false;
			if(mbHasPanned) STOP_EVENT();
			break;
		case MOUSE_RIGHT:
			mbIsScrolling = false;
			if(mbHasScrolled) STOP_EVENT();
			break;
	};
};


void PannableCamera::BindToKeyboardAndMouse() {
	GameState *pState = Kernel::GetInstance().GetGameStatePtr();
	
	pState->BindEvent(new KeyDownEvent(InputKey(InputKey::UP, NULL)),
		(new ObjFunctor<PannableCamera, void, const fVector2D&>(this, &PannableCamera::Scroll))->Bind(fVector2D(0, -1)));
	pState->BindEvent(new KeyDownEvent(InputKey(InputKey::DOWN, NULL)),
		(new ObjFunctor<PannableCamera, void, const fVector2D&>(this, &PannableCamera::Scroll))->Bind(fVector2D(0, 1)));
	pState->BindEvent(new KeyDownEvent(InputKey(InputKey::LEFT, NULL)),
		(new ObjFunctor<PannableCamera, void, const fVector2D&>(this, &PannableCamera::Scroll))->Bind(fVector2D(-1, 0)));
	pState->BindEvent(new KeyDownEvent(InputKey(InputKey::RIGHT, NULL)),
		(new ObjFunctor<PannableCamera, void, const fVector2D&>(this, &PannableCamera::Scroll))->Bind(fVector2D(1, 0)));
	
	pState->BindEvent(new MouseDownEvent(),
		EventHandler(new ObjFunctor<PannableCamera>(this, &PannableCamera::OnMouseDown), EventHandler::PRIO_GAME_HIGH));
	pState->BindEvent(new MouseMoveEvent(),
		EventHandler(new ObjFunctor<PannableCamera>(this, &PannableCamera::OnMouseMove), EventHandler::PRIO_GAME_HIGH));
	pState->BindEvent(new MouseUpEvent(),
		EventHandler(new ObjFunctor<PannableCamera>(this, &PannableCamera::OnMouseUp), EventHandler::PRIO_GAME_HIGH));
};



/**** World Camera ****/

WorldCamera::WorldCamera(const tVector &pos, const tVector &dir, const tVector& up)
: Physical(tCreationOptions(), WtfGraphics::GraphicsData::tCacheRef(), new PhysicalModels::Sphere("", 0.1)) {
	this->setPosition(pos);
	//this->setOrientation(
};

void WorldCamera::EnterPerspective(const IScreen::tRef& pScreen)
{
	tVector pos, dir(0,0,-1), up(0,1,0);
	
	pos = this->getPosition();
	dir = this->getOrientation() * dir;
	up = this->getOrientation() * up;
	
	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();

	// Camera setup
	gluPerspective(45.0f, // 45 degree field of view
		(tScalar)(pScreen->getSize().x()) / (tScalar)(pScreen->getSize().y()),	// Aspect ratio
		0.1f, 2000.0f);	// Near and far clipping planes
	gluLookAt(pos[0],pos[1],pos[2],
			dir[0] + pos[0],dir[1] + pos[1],dir[2] + pos[2],
			up[0],up[1],up[2]);

	glMatrixMode(GL_MODELVIEW);
};

void WorldCamera::LeavePerspective()
{
	glMatrixMode(GL_PROJECTION);
	glPopMatrix();
	glMatrixMode(GL_MODELVIEW);
};
